Chapter 16
Using MFC and ATL

by Kenn Scribner

In This Chapter

  What Is ATL and Why Is It Important for Programming in MFC? 590
  Helpful ATL COM Support for MFC Applications 591
  Advanced ATL Support for MFC Applications 604

What Is ATL and Why Is It Important for Programming in MFC?

If you’ve never developed COM components in plain C++, there are many reasons why using the Active Template Library (ATL) from within an MFC application makes sense. Before you learn about the specifics of COM, however, step back in time a moment and remember how programmers used to develop Windows applications.

Imagine yourself writing a large-scale Windows program, but in 1990. “What, no MFC?” No, you cannot use MFC—the MFC framework hadn’t been written yet. (Or at least MFC hadn’t been released to the general development community.) You must write code to handle each and every Windows message you want to process. You crack every WM_COMMAND, read every lParam, and generally write a ton of grungy support code. For your efforts, you have total control over the flow and operation of your application. On the other hand, those same efforts cost you time to market, time to reach “code complete,” and more bugs (or “features”) than anyone should have to face.

This is the thrust behind MFC, or at least one of the main goals. You fire up Developer Studio, click a few buttons, and whammo! you have a working Windows application. The application initially lacks many of the features your low-level design would have you incorporate, but you didn’t spend a lot of time creating the basic framework to support those same features. You get right to the meat of the application much more quickly, and you can feel quite confident the basic framework code is relatively bug-free.

Well, this is the same argument I would make regarding the history of ATL and COM. “Pure” COM, written in C++ (or C, if you’ve really been doing it a long time), is just as detail-oriented and bug-prone as Windows code using the old Software Development Kit. In fact, you could easily find COM programmers who would tell you it’s more detailed-oriented and bug-prone. And they’d be correct—COM programming can be far more challenging (and that’s “challenging” in the “difficult” sense). The simple stuff isn’t so bad, but the truly brilliant code takes work.

Microsoft took hundreds of man-years of COM development experience and wrapped it up in ATL. What’s even more intriguing is the T in ATL: template We’re literally talking C++ templates here, which are recent and exciting additions to the C++ specification. Not only will you save time coding your application, but you’ll also gain those hundreds of man-years of Microsoft coding experience and be at the forefront of C++ development and code reuse technology.

Helpful ATL COM Support for MFC Applications

You will begin your examination of ATL within an MFC application by looking at ways ATL might help you manage a few of the details COM requires you to handle. First, you will examine the COM object’s pointer itself and how you might better tackle the object’s reference count. Then, you will look more deeply into both COM binary strings, or BSTRs, and converting textual information from Unicode (wide characters) to something more easily handled programmatically. Finally, you will unlock some of the secrets behind the VARIANT and see how ATL can help you there as well. Let’s begin with COM pointers.

COM Pointers the Smart Way

When you work with COM, and in particular some specific COM object, all you are really doing is accessing the given object’s member functions through a pointer. Sure, the “plumbing” is a bit more extravagant with COM, as your pointer could actually access an object outside of your current address space, or even on a different computer over a network connection. But it’s still just a pointer, and you call functions using the pointer just as you always have in the past. To you, the object’s consumer, this code is equivalent:

pObject1->Foo(); // a C++ object

and:

pObject2->Bar(); // a COM object

Yes, the objects are of different types (a C++ object versus a COM object), but you’re accessing the member function in the same manner—through a pointer.

But when using COM objects, you must be cognizant of the object’s reference count (as you saw in Chapter 10, “COM”). In C++, you would delete the object. When using COM, you release it by using the object’s Release() method (which is guaranteed by the rules of COM to exist). To handle the situations where you don’t want to forget to delete the object (or release it), smart pointers were created. A smart pointer is a C++ class that encapsulates the actual memory pointer. The true beauty of a smart pointer is that it can handle the object deletion for you when the smart pointer goes out of scope. At that time its destructor is called, and it may then delete or release the constituent object for you. This relieves you of the burden of doing it yourself. By definition, you can’t “forget” to delete or release the object. The C++ language handles the details for you.

Memory leaks are bad enough. But rogue COM objects can be truly nasty (your COM object becomes “rogue” when your pointer variable leaves its scope without releasing the object). These COM objects might be dynamic link libraries (DLLs) loaded into your address space, munching your valuable virtual and resource memory, or they might be actual processes, consuming CPU cycles and system resources you should be using. They can even hang the system. Therefore, it’s usually desirable to use a smart pointer when dealing with COM objects. Why invite disaster?

ATL has the ideal solution—a smart pointer template. Actually, there are two major varieties of ATL smart COM pointer templates, and you select the most appropriate based on the mechanics of obtaining the COM pointer. If you create the object from dust, so to speak, you’ll use the CComPtr template class:

template< class T > class CComPtr

On the other hand, if you’re calling IUnknown::QueryInterface() (see Chapter 10) though a COM object pointer you have already obtained, you’ll use ATL’s CComQIPtr template:

template< class T, const IID* piid > class CComQIPtr



To make more sense of these odd-looking definitions, let’s look at some code. In Chapter 23, “MFC and DHTML,” I describe the use of Dynamic HTML, or DHTML, from within an MFC application. There you will request an IDispatch pointer from the Internet Explorer Web browser control using code much like the following:

CComPtr<IDispatch> pDocDispatch;
HRESULT hr = m_pBrowser->get_Document(&pDocDispatch);
if ( FAILED(hr) || pDocDispatch.p == NULL ) {
TRACE(“No active document...load failed.\n”);
return;
} // if

Here you see an example of a CComPtr-based smart COM pointer. The Web browser ActiveX control will provide you with its current document (a COM object), which you access through its base interface, IDispatch.

Given that you were able to obtain the document interface pointer—that is, the pDocDispatch pointer is valid—you probably want to access its IHTMLDocument2 pointer, which is where you start burrowing into the DHTML aspects of the HTML document. To do that, you must call the object’s QueryInterface() method for its HTML document pointer:

CComQIPtr<IHTMLDocument2,&IID_IHTMLDocument2>
ÄpHTMLDocument2(pDocDispatch);
if ( pHTMLDocument2.p == NULL ) {
TRACE(“IHTMLDocument2 interface not supported.\n”);
ASSERT(FALSE);
return;
} // if

You should note two things in particular with this example. First, the code to perform the QueryInterface() is encapsulated in the CComQIPtr template, making the code more concise (and bug-free). Second, if for some reason you were unable to obtain the IHTMLDocument2 pointer from the Web control’s document pointer, you needn’t worry about calling pDocDispatch’s Release() method. CComPtr handles this detail for you. When pDocDispatch goes out of scope, the COM object is released for you automatically.


Tip:  

At certain times, you probably do not want to use a smart pointer for your COM work, such as during a tight loop of some kind (you probably won’t want to sustain the overhead of construction and destruction). If it makes sense to use such a tool, use the smart pointer. Otherwise, forgo it and keep track of the COM object’s reference count yourself. In real-world applications you’ll usually find a mixture of both.



Note:  

If you use the smart pointer templates, you won’t be required to link with ATL.LIB (which provides, among other things, self-registration support). You will, however, need to include atlbase.h, preferably in your stdafx.h file:

#include <atlbase.h>

One place where the smart COM pointer makes a lot of sense to use is when using C++ exception handling (see Chapter 22, “Exceptions”). If you declare the COM smart pointer within a try block, and an exception is somehow thrown, the smart pointer handles the object’s release for you. You don’t need to worry about the COM object’s reference count yourself. This naturally leads to more exception-aware code and alleviates your need to code even more defensively.

Other ATL COM Support

You’ve just examined two of the most important ATL templates—after all, if you don’t have a pointer to a COM object, you don’t have a COM object. But there are at least several other useful areas where ATL can be of benefit. The first is when dealing with the BSTR (“B-stir”), which is a specialized COM string you’ll look at more closely in a moment. A second area is when dealing with Unicode (wide-character) strings and their conversions, as COM is completely Unicode-aware (even on Windows 95 and 98 systems). These useful conversion functions are there to use when COM hands you a Unicode string. And a third area is ATL assistance with VARIANTs, which are data structures used as parameters in COM method calls (especially for scripting purposes).

The BSTR

Before you see what a BSTR is composed of, first examine a generic wide-character string. Unicode (wide-character) strings are essentially just strings that require more than a single byte to encode their constituent character data. That is, when you deal with the ANSI character set, all characters, printable or not, range from 0-127. Although in some cases you also include the upper characters from 128-255, the point is the same. The character can be encoded in a single 8-bit byte.

Some languages, especially those with more idiomatic character sets, require more than 256 characters to comprise their alphabets (such as Kanji, a Japanese alphabet). Naturally, 8 bits won’t do to describe a single character in such an alphabet. To solve this, Unicode was invented. Essentially, Unicode uses two bytes for encoding instead of one (there is more to it than that, but now you understand the basic data storage requirements).


Note:  

As with almost any rule, there are exceptions. On the Apple PowerMac, Unicode strings are based on single-byte characters.


A BSTR, then, is a binary data storage medium that consists of three main parts: the length (4 bytes), the Unicode string, and a final NULL terminator (a single byte). This is depicted in Figure 16.1. It is perfectly legal for the Unicode string to contain NULL characters, so don’t simply examine memory for zero-value bytes expecting to find the end of the string.


Figure 16.1  BSTR memory layout.

The conversion implications alone might make you stop and think a moment—what do you do in Windows 95/98, where you don’t have Unicode? Outside of COM you also have to deal with the Win32 API calls to allocate and deallocate BSTR variable memory. This can be painful at times.

ATL provides one mechanism for a more programmer-friendly use of the BSTR. It provides the CComBSTR class, which you’ll find defined in atlbase.h but implemented in atlimpl.cpp.


Note:  

As with the smart pointer templates, you won’t be required to link with ATL.LIB to use CComBSTR, but you will need to include atlimpl.cpp once somewhere in your source files, preferably in stdafx.cpp:

#include <atlimpl.cpp>

To see how you might use CComBSTR, and why it’s so useful, consider this COM method from this chapter’s first sample, ATLServer, shown in Interface Description Language (IDL) in Chapter 10:

[helpstring(“method Encrypt”)] HRESULT Encrypt(
Ä[in] BSTR bstrClearText, [out] BSTR* pbstrMunged);



This function accepts a BSTR and a pointer to a BSTR. The first BSTR is encrypted and stored into the second. Without CComBSTR support, you might use the Encrypt function like this from the MFC application as seen in Listing 16.1.

Listing 16.1 Manual BSTR Allocation and Deallocation


// Now that you have your COM object, you create some BSTRs and call it
BSTR bstrClearText = ::SysAllocString(L”123abc”);
BSTR bstrMunged; // will be filled by COM object
hr = pEncrypt->Encrypt(bstrClearText,&bstrMunged);
if ( FAILED(hr) ) {
// Problem encrypting the string...
AfxMessageBox(“Error encrypting string!”,MB_OK | MB_ICONERROR);
::SysFreeString(bstrClearText);
::SysFreeString(bstrMunged);
return;
} // if
... // do something
::SysFreeString(bstrClearText);
::SysFreeString(bstrMunged);

Doesn’t look that scary? Let’s look a bit more closely. First, you can’t simply do a new to allocate the memory for the BSTR. You must use the Win32 API SysAllocString, which has this prototype:

BSTR SysAllocString( OLECHAR FAR* sz);

So not only can’t you use new to allocate the memory for the BSTR, but you also can’t even pass in to SysAllocString a simple char*-based string! You have to pass in some nasty thing known as an OLECHAR (which is, in reality, a Unicode string, hence the L positioned before the literal).

Given that you’ve properly created your BSTR, you pass it to the COM object. When the Encrypt method has completed its task, you’re done, right? Nope. Just as you used a special function to create the BSTR in the first place, you must also use a special Win32 API function to deallocate the BSTR memory, SysFreeString:

HRESULT SysFreeString( BSTR bstr);

It’s messy because you have to place the same code in two (or more) locations to perform the deallocation—upon successful completion and upon failure. The problem you face if you do this incorrectly is the potential memory leak from improper deallocation of the BSTR. Consider what would happen if, for some reason, the intervening code between the allocation and deallocation of the BSTR threw an exception (see Chapter 22 for details regarding exception handling). The BSTR would be left in memory until the computer was shut down.

A similar example using CComBSTR would look like this (taken from this chapter’s MFCClient sample):

// Now that you have your COM object, you create some BSTRs and call it
CComBSTR bstrClearText(m_strOriginal); // a CString...
CComBSTR bstrMunged; // will be filled by COM object
hr = pEncrypt->Encrypt(bstrClearText,&bstrMunged);
if ( FAILED(hr) ) {
// Problem encrypting the string...
AfxMessageBox(“Error encrypting string!”,MB_OK | MB_ICONERROR);
return;
} // if

This looks much better. In this case, you use a simple CString variable to contain your programmer-friendly text. You pass that, as a LPCTSTR value, to CComBSTR, which automatically performs the SysAllocString for you (after converting the TCHAR-based string to an OLECHAR-based string). What’s even better is that the CComBSTR destructor performs the SysFreeString for you too. Now, when the CComBSTR variable goes out of scope, for whatever reason, the BSTR itself (contained within the CComBSTR class) is deallocated. Exceptions are no longer problematic.


Note:  

CComBSTR is not the only class available to you for friendly BSTR programming. You can also use the _bstr_t class found in comutil.h. I’m using CComBSTR here simply because it’s part of the ATL.


If smart pointers and BSTR assistance were all ATL provided, it would probably suffice. However, how do you convert an OLECHAR to something you can actually use? ATL helps here too.

Wide-Character Conversions

CComBSTR had to have some mechanism for converting the LPCTSTR to an OLECHAR string. By using the ATL conversion macros, you can convert just about any string type to just about any other string type. They follow this basic pattern:

MACRONAME(string_address)

The macro itself is named according to this convention:

{from type}2{to type}

So, converting from a TCHAR (LPTSTR) string to an OLECHAR string would use the T2OLE macro:

USES_CONVERSION; // only required once per MFC member function
TCHAR strTString[] = _T(“My TCHAR string.”);
OLECHAR* lpOleString = T2OLE(strTString);

The USES_CONVERSION macro defines a private function that actually performs some of the conversions. To avoid compiler errors, it’s best to put it at the beginning of each and every function that uses the conversion macros.


Note:  

Interestingly, neither atlbase.h nor atlimpl.cpp are required to use the conversion macros. Instead, you may simply include atlconv.h in the specific MFC source file where the macros will be used, if the conversion macros are all that interests you.


The conversion macros take their direction from the current compiler settings. That is, if both the from and to types are the same, no conversion is required (a value is simply returned). How would you know if a conversion would take place, or that the from and to string types were the same? Table 16.1 shows how the T and OLE types are converted based upon the current compiler settings.

Table 16.1 Conversion Macro Compiler Type Assignments

Compiler Directive T Becomes OLE Becomes

None ANSI WIDE
OLE2ANSI ANSI ANSI
_UNICODE WIDE WIDE
OLE2ANSI and _UNICODE WIDE ANSI

Essentially, all you really need to do is select the starting type, the ending type, and decide if the ending type is const (in which case you add a C to the macro name, such as A2COLE for converting an ANSI string to a const OLESTR). The compiler directives in place will resolve the correct starting and ending types for you. Table 16.2 gives you the complete set of conversion macros. Here A indicates ANSI (or a standard char*-based sting), T indicates a TCHAR-based string, W is a wide-character string, OLE is an OLESTR-based string, and BSTR is self-explanatory.



Table 16.2 ATL Conversion Macros

Macro Conversion

A2BSTR (LPSTR to BSTR)
OLE2A (LPOLESTR to LPSTR)
T2A (LPSTR/LPOLESTR to LPSTR)
W2A (LPWSTR to LPSTR)
A2COLE (LPSTR to LPCOLESTR)
OLE2BSTR (LPOLESTR to BSTR)
T2BSTR (LPSTR/LPOLESTR to BSTR)
W2BSTR (LPWSTR to BSTR)
A2CT (LPSTR to LPCTSTR/LPCOLESTR)
OLE2CA (LPOLESTR to LPCSTR)
T2CA (LPSTR/LPOLESTR to LPCSTR)
W2CA (LPWSTR to LPCSTR)
A2CW (LPSTR to LPCWSTR)
OLE2CT (LPOLESTR to LPCSTR/LPCOLESTR)
T2COLE (LPSTR/LPOLESTR to LPCOLESTR)
W2COLE (LPWSTR to LPCOLESTR)
A2OLE (LPSTR to LPOLESTR)
OLE2CW (LPOLESTR to LPCWSTR)
T2CW (LPSTR/LPOLESTR to LPCWSTR)
W2CT (LPWSTR to LPCSTR/LPCOLESTR)
A2T (LPSTR to LPSTR/LPOLESTR)
OLE2T (LPOLESTR to LPSTR/LPOLESTR)
T2OLE (LPSTR/LPOLESTR to LPOLESTR)
W2OLE (LPWSTR to LPOLESTR)
A2W (LPSTR to LPWSTR)
OLE2W (LPOLESTR to LPWSTR)
T2W (LPSTR/LPOLESTR to LPWSTR)
W2T (LPWSTR to LPSTR/LPOLESTR)

Now that you’ve seen that ATL provides string conversion assistance, you can see how useful it is, especially when dealing with COM or general-purpose internationalization issues. There is also another area in which ATL can help your COM programming task. ATL simplifies working with VARIANT data.

VARIANT

VARIANT is a somewhat mysterious datatype to many strictly MFC programmers. The reason is easy to see: A VARIANT is a purely COM-based datatype. It’s also specific to the COM scripting architecture. But if you program in COM at all, you’ll run across the VARIANT. So, to clear up the mystery, step back a minute and think about COM and the COM architecture. Then I’ll describe the VARIANT and how ATL can help you work with VARIANT data.

Ultimately, COM provides a data-sharing mechanism. COM is more than that, but at the lowest level, COM simply provides a conduit for sharing data. If you’re using an in-process COM server (a DLL), you have no worries. All the data passed between your main application and the COM DLL is created and used within the same virtual address space.

But have you ever tried to send data from one executable (process) to another? How did you do it? Named pipes? Memory-mapped files? Perhaps simple data embedded in a WPARAM or LPARAM of a custom message? All of these work and have their place. COM, on the other hand, acts as a facilitator to provide you with a highly optimized and relatively automatic data transfer mechanism. Clearly COM does much more than that, but the underlying data-passing mechanisms are critical to COM’s functionality.

When COM takes data from one address space and sends it to another (same machine or networked), the process of converting the data is called marshaling. Nearly all types of data can be marshaled without too much trouble, though some datatypes are more difficult to marshal than others are. For example, think about a pointer. A pointer, after all, is just another number. But it represents an address of some particular piece of information, and that address is only valid in a specific address space. If you create a memory-mapped file, the (virtual address) pointers used to access the data in the two application’s address spaces will be different, even though the physical address of the data is the same.

What does this have to do with VARIANTs? Everything, as it happens. COM’s architects, or possibly the Visual Basic team (it’s unclear to me which), sat down and compiled a list of “standard” datatypes COM would automatically “know” how to marshal. With no further work on your part, if you declare your interface member parameters to be of datatypes from this list, you are relieved from writing the marshaling code yourselves (an arduous process for most people). For example, you don’t need to concern yourselves with how a DATE structure is passed from address space to address space—you simply send the DATE information on its merry way, allowing COM to handle the gritty details for you.



Listing 16.2 and the individual items it contains collectively make up the VARIANT datatype list. The VARIANT itself is simply a discriminated union that contains an integer denoting the type (the discriminator) and a union containing all of the possible datatypes. You assign the integer an enumerated value that indicates what data is stored in the union, and then assign the data itself.

Listing 16.2 The VARIANT Discriminated Union


typedef struct tagVARIANT{
VARTYPE vt;
WORD wReserved1;
WORD wReserved2;
WORD wReserved3;
union {
      long          lVal;           /* VT_I4                */
      unsigned char bVal;           /* VT_UI1               */
      short         iVal;           /* VT_I2                */
      float         fltVal;         /* VT_R4                */
      double        dblVal;         /* VT_R8                */
      VARIANT_BOOL  boolVal;        /* VT_BOOL              */
      SCODE         scode;          /* VT_ERROR             */
      CY            cyVal;          /* VT_CY                */
      DATE          date;           /* VT_DATE              */
      BSTR          bstrVal;        /* VT_BSTR              */
      IUnknown      punkVal;        / VT_UNKNOWN            */
      IDispatch     pdispVal;       / VT_DISPATCH           */
      SAFEARRAY     parray;         / VT_ARRAY|*            */
      unsigned char pbVal;          / VT_BYREF|VT_UI1       */
      short         piVal;          / VT_BYREF|VT_I2        */
      long          plVal;          / VT_BYREF|VT_I4        */
      float         pfltVal;        / VT_BYREF|VT_R4        */
      double        pdblVal;        / VT_BYREF|VT_R8        */
      VARIANT_BOOL  pbool;          / VT_BYREF|VT_BOOL      */
      SCODE         pscode;         / VT_BYREF|VT_ERROR     */
      CY            pcyVal;         / VT_BYREF|VT_CY        */
      DATE          pdate;          / VT_BYREF|VT_DATE      */
      BSTR          pbstrVal;       / VT_BYREF|VT_BSTR      */
      IUnknown      **ppunkVal;     /* VT_BYREF|VT_UNKNOWN  */
      IDispatch     **ppdispVal;    /* VT_BYREF|VT_DISPATCH */
      SAFEARRAY     **pparray;      /* VT_BYREF|VT_ARRAY|*  */
      VARIANT       pvarVal;        / VT_BYREF|VT_VARIANT   */
      void          * byref;        /* Generic ByRef        */    };
} VARIANT, VARIANTARG;

The discriminator, VARTYPE, is defined in this manner:

typedef unsigned short VARTYPE;

If used properly, it will contain an enumeration from VARENUM as seen in Listing 16.3.

Listing 16.3 The VARENUM Discriminators


enum VARENUM
{   VT_EMPTY            = 0,
   VT_NULL              = 1,
   VT_I2                = 2,
   VT_I4                = 3,
   VT_R4                = 4,
   VT_R8                = 5,
   VT_CY                = 6,
   VT_DATE              = 7,
   VT_BSTR              = 8,
   VT_DISPATCH          = 9,
   VT_ERROR             = 10,
   VT_BOOL              = 11,
   VT_VARIANT           = 12,
   VT_UNKNOWN           = 13,
   VT_DECIMAL           = 14,
   VT_I1                = 16,
   VT_UI1               = 17,
   VT_UI2               = 18,
   VT_UI4               = 19,
   VT_I8                = 20,
   VT_UI8               = 21,
   VT_INT               = 22,
   VT_UINT              = 23,
   VT_VOID              = 24,
   VT_HRESULT           = 25,
   VT_PTR               = 26,
   VT_SAFEARRAY         = 27,
   VT_CARRAY            = 28,
   VT_USERDEFINED       = 29,
   VT_LPSTR             = 30,
   VT_LPWSTR            = 31,
   VT_RECORD            = 36,
   VT_FILETIME          = 64,
   VT_BLOB              = 65,
   VT_STREAM            = 66,
   VT_STORAGE           = 67,
   VT_STREAMED_OBJECT   = 68,
   VT_STORED_OBJECT     = 69,
   VT_BLOB_OBJECT       = 70,
   VT_CF                = 71,
   VT_CLSID             = 72,
   VT_BSTR_BLOB         = 0×fff,
   VT_VECTOR            = 0×1000,
   VT_ARRAY             = 0×2000,
   VT_BYREF             = 0×4000,
   VT_RESERVED          = 0×8000,
   VT_ILLEGAL           = 0×ffff,
   VT_ILLEGALMASKED     = 0×fff,
   VT_TYPEMASK          = 0×fff
};.

As you know, a union is always allocated enough memory to contain the largest datatype within its scope. Therefore, a VARIANT always has the same memory footprint. The “universal marshaler,” then, merely has to treat the VARIANT as a chunk of memory, of known size, and be able to interpret the various VARENUM types. You leave this to COM. If you select parameter types from the VARENUM list, you have no marshaling problems. However, if you select something else for your parameter type, such as a Win32 POINT structure, you have to handle the marshaling yourselves.


Tip:  

Strive to select your parameter types from the VARENUM list if you are writing an in-process server. If you do, you won’t need to compile a separate “proxy/stub” DLL to perform the custom marshaling, which reduces the number of files you’ll need to ship and install. (Local servers will require a proxy/stub DLL in any case, as you’ll see shortly.)


The CComVariant template handles the details for you. Without such help, you need to allocate (and deallocate) memory for the discriminated union, assign the discriminator, and handle setting the union value. Granted, it’s bread and butter for highly skilled C++ programmers such as yourself, but why write all of that redundant code? Save your typing skills for the really cool algorithms.

With CComVariant, you simply declare a variable and pass in the VARIANT type:

CComVariant varError(VT_ERROR);

You’ve written much less code, and the code is less error-prone than dealing with the VARIANT discriminated union directly.


Note:  

Dual interfaces, which are COM interfaces that inherit from IDispatch rather than directly from IUnknown, by definition use VARIANTs to pass method parameters. Many of the newer COM technologies use dual interfaces, like ActiveX and OLE DB. VARIANTs are then unavoidable, which makes VARIANT support code that much more valuable. You don’t have to use ATL, as you could use MFC’s COleVariant, but if you’re already using ATL for other reasons, it may make sense to stick with it, depending upon your application.


All of these ATL support templates and conversion macros are exceptionally helpful, and you’ll probably find if you’re working with MFC and COM objects they’re invaluable. But what about the case where your MFC application needs to expose an interface or two itself? ATL can help there, too.



Advanced ATL Support for MFC Applications

Ignoring the potentially networked COM object, there are essentially two types of COM servers, in-process (DLLs) and local (EXEs). If your requirements dictate a simple in-process server, by all means choose your favorite COM implementation methodology (like ATL) and code away. Sometimes, however, a local server fills the requirement(s) more elegantly. In this case, you might still choose to use ATL to implement your COM server, but there are strong arguments in favor of using MFC, too. For one, MFC has a rich and robust architecture with plenty of support classes. But MFC’s implementation of COM is limited because of this basic MFC tenet—no multiple inheritance. Sure, you can do just about anything COM-wise with MFC, but the MFC architecture isn’t quite as natural as ATL’s is to a COM programmer.

Why would I claim this? Well, MFC implements COM interfaces as nested classes. The MFC architects decided this was the easiest and most straightforward way to implement COM, and it works. But COM interfaces usually have an “is-a” relationship, not a “has-a” relationship. For illustration purposes, the (imaginary) IMyInterface interface “is-a” IDispatch interface, whereas IDispatch “is-a” IUnknown interface, and so on. Therefore, multiple inheritance, from a COM-based architectural standpoint, makes more sense than a nested class, which is a class contained within some enclosing class. The enclosing class “has-a” nested class.

Perhaps, then, you could write most of your basic local server code in MFC, yet expose your COM interface(s) using ATL. As it happens, this is not only possible, but it is also a great way to use the best features of both tools.

Begin with Your MFC Application

Perhaps the best way to get started is to concentrate on your MFC application first, and then add the requisite ATL COM support later (as you’ll see). This example simply increments a counter in the COM object when called by a client application. True, it’s not a tremendously exciting example, but this way, the example isn’t cluttered with code that’s not germane to mixing ATL with MFC.

The MFCServer example is a very basic MFC SDI application that has the addition of a single static attribute in the CWinApp-derived class (CMFCServerApp). This long-valued attribute will hold the number of “hits” the server sustained through its COM interface. The basic MFC framework was created in the usual fashion; nothing special, in this case.


Tip:  

Be sure that when you create the MFC application using the MFC AppWizard, you accept the default setting for automation (which is unchecked, for “no automation support”). The automation aspects of your program are precisely what you’re about to add using ATL, so you don’t want old MFC automation code causing problems further down the road.


Assuming you have your MFC application running and debugged, or at least nearly so, let’s see how to actually add the COM interface.

Add the Required ATL Support

Stepping back for a moment, you now have an MFC application to which you intend to add ATL code. Do you want to write this ATL code yourselves, or is there some other way to produce it for you automatically? As it happens, there is. The ATL Object Wizard naturally provides you with robust ATL code. Visual C++ 6 has a new feature you can use to automatically add the ATL support required. In a moment, you’ll go through the process step by step. Before you start, here’s a quick road map. When you get bogged down in the details, you’ll at least have some idea where you are in the process and how much more remains to be done.

Here are the steps you will follow to add full-fledged ATL support:

  Create (and examine) the ATL COM support code using the ATL Object Wizard.
  Manually add/edit the files you require that the Object Wizard didn’t produce for you.
  Modify your project settings to automatically create and register your proxy/stub DLL.
  Write a client (test) application to check out your COM object.

After you’ve added the ATL COM support, you will examine the code the wizard inserted. After all, it’s not usually a good idea to accept wizard-based code without some understanding of its functionality.

Creating the ATL COM Support Code Automatically

MFC programmers are quite used to running the MFC ClassWizard to add methods to their C++ classes. Invoking the ClassWizard couldn’t be easier: Select it from Developer Studio’s View menu or right-click the mouse in an edit window. But the ATL Object Wizard doesn’t share such an obvious user-interface mechanism. To invoke it, you right-click the project name from the project’s Workspace window (see Figure 16.2). The associated context menu will have a menu option for New ATL Object, and it’s this menu item you select to actually bring up the wizard.


Figure 16.2  Developer Studio’s Workspace window.


Note:  

Developer Studio may inform you that the ATL code insertion failed after you first invoke the ATL Object Wizard. Don’t believe it! It’s a bug Microsoft is examining (at the time of this writing). Go ahead and invoke it a second time and add your new ATL object just as if you hadn’t seen the error dialog.


When you invoke the ATL Object Wizard, your MFC application (CWinApp-derived) class will be modified and ATL-specific files will be added to your project (assuming you click Yes when asked for confirmation). This is enough of a framework to actually get ATL up and running within your application. When the wizard has finished processing your existing MFC files, it will ask you for the type of COM object you want to insert (see Figure 16.3). You can select from a wide variety of COM object types; in this case, you will select the simple COM object and click Next.


Figure 16.3  The ATL Object Wizard.

After you have decided what type of COM object you want, the wizard will ask you for the specific COM interface information it requires to build the ATL-specific files. It displays the object properties property sheet, as shown in Figure 16.4.


Figure 16.4  The ATL Object Wizard’s Names property sheet.

Because this is an MFC book, this particular ATL wizard won’t be described in tremendous detail—there is a lot there for such a simple-looking dialog box. Essentially, though, you will name the interface and source files in the first property sheet and establish the object’s operating parameters in the second.

The first property sheet is the Names sheet, as you see in Figure 16.4. On this sheet you provide a basic name string in the Short Name edit control. The text you type is propagated to the other seven edit controls as default values. Feel free to change this text if you want. Here you provide such basic information as the name of your interface, the ProgID, the name of your CoClass, and the ever-present C++ filenames (declaration and implementation).

The second property sheet might at first seem to be fairly simple, but you’re about to make decisions with huge implications when you select each option (see Figure 16.5), so choose carefully.


Figure 16.5  The ATL Object Wizard’s Attributes property sheet.



All these implications cannot be adequately described here. It’ll have to suffice to say you want an apartment-threaded object with a custom interface that does not support aggregation. What that basically says is you want a COM object that uses a regular Windows message queue for thread synchronization (apartment threading), is not an automation object (the custom versus dual interface), and is not willing to have its interfaces “consumed” by another COM object (aggregation). The deeper meaning is that you want a COM object that

  Does not require you to use multithreaded synchronization mechanisms, like critical sections and/or mutexes
  Does not have an interface that inherits from IDispatch (dual) versus simply IUnknown (custom)
  Won’t allow another COM object to control access to your interfaces

After you’ve selected these attributes and clicked OK, the wizard will actually add code to your basic ATL framework. At this time you have a simple COM object with a single custom interface. But the interface has no methods. You now need to add those, just as you add message handlers to your MFC classes. For this, you again ignore MFC’s ClassWizard (which can’t help you with ATL code) in lieu of the context menu in the Workspace window. Just as you right-clicked to invoke the ATL Object Wizard, you will right-click to add methods and properties.

Here, it makes a difference where you right-click. In Figure 16.6, you see both C++ classes and COM interfaces in the tree control, where before (in Figure 16.2) there were only C++ classes. If you right-click on a C++ class, you can add member functions and variables, virtual functions, and message handlers. On the other hand, if you right-click on a COM interface (denoted by a leading I in the name), you can add properties and methods. It’s a method you want to add to your interface, so select that menu option to activate the dialog depicted in Figure 16.7.


Figure 16.6  The Developer Studio Workspace window revisited.


Figure 16.7  The ATL Add Method to Interface dialog.

You are essentially designing the interface, or API, for your COM object. The interface you design—its methods and parameters—determines the functionality your object provides to other objects and applications. For this simple example, you want a method to increment a counter and somehow provide the new value to you. You can do this by adding a method called BumpCount that returns something known as an HRESULT. The HRESULT is a 32-bit COM return code comprised of several component fields that together serve to indicate the success or failure of the COM method call from a COM perspective (refer to any good COM text for the specific field meanings and values). If your object was successfully called with valid parameters, it will return a successful result code. From a COM perspective, the method call was a success.

In this case, it makes sense to pass into the method a pointer to the client’s integer variable used to accept the new counter value. The client will create your object, call BumpCount(), and then typically check the HRESULT to see if COM was happy. If the HRESULT was successful, the client will then do something with the count value it received.

Based on these design ideas, your interface method would look something like this:

BumpCount(long * piCount)

This is what you would type into the Add Method to Interface dialog’s Parameters edit control, with the addition of the IDL directional tags [in] and [out]. With the IDL tags, your parameter list would look like this:

BumpCount([out] long * piCount)


Note:  

The IDL parameter tags add semantics to the parameter list. They tell the COM marshaling functions the direction of data flow. The marshaling functions require this information, as these functions are responsible for actually passing data between processes. This “data passing” is quite a trick: How do you represent a pointer to the same thing in two different address spaces? A good COM book will tell you why this is necessary and how it is done.


in parameter data is something akin to read-only, or const. The data comes into your address space and is dropped off at the secretary’s desk for further processing, so to speak. You grab it, you use it. (This does not relieve you from your responsibility to deallocate any BSTRs you are passed, however.) The out parameters, however, are far more interesting. They’re always pointers. You fill whatever they are pointing to with outgoing data, which in this case is simply a long result. To be somewhat more complete, there is also “in-out” data, which I’ll leave to a good COM textbook for further explanation.

After you’ve clicked OK, the wizard will add the necessary code to the many places within your framework it is required to declare and implement the BumpCounter method. If you were continuing on in ATL, you would now add the code to actually handle the counter.

In this case, though, all you really wanted was to have as much code automatically generated for you as was possible, so you will stop with your ATL wizard work and turn again to your MFC project. You’ll be visiting many of the various ATL source code files, however, for editing and revising as you more fully integrate ATL into your MFC project.



Examining the ATL Code Added to Your MFC Application

Now that you have the basic ATL code inserted, it’s time to tweak and modify what you were given. Turning to your example, from Developer Studio open the file MFCServer.cpp. As you can see from Listing 16.4, the wizard added quite a bit of code automatically (shown in italics). This code is required to initialize COM (and ATL) and provides the basic plumbing your COM server will require.

Listing 16.4 MFCServer.cpp with ATL Support


// MFCServer.cpp : Defines the class behaviors for the application.
//

#include “stdafx.h”
#include “MFCServer.h”

#include “MainFrm.h”
#include “MFCServerDoc.h”
#include “MFCServerView.h”
#include <initguid.h>
#include “MFCServer_i.c”
#include “MFCCOMServer.h”

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

////////////////////////////////////////////////////////////////////
// CMFCServerApp
(Message mapping removed for clarity...)

////////////////////////////////////////////////////////////////////
// CMFCServerApp construction
long CMFCServerApp::m_lnNumHits = 0;
CMFCServerApp::CMFCServerApp()
{
// TODO: add construction code here,
// Place all significant initialization in InitInstance
}

////////////////////////////////////////////////////////////////////
// The one and only CMFCServerApp object
CMFCServerApp theApp;

////////////////////////////////////////////////////////////////////
// CMFCServerApp initialization
BOOL CMFCServerApp::InitInstance()
{
if (!InitATL())
return FALSE;
AfxEnableControlContainer();
// Standard initialization
// If you are not using these features and wish to reduce the size
//  of your final executable, you should remove from the following
//  the specific initialization routines you do not need.

#ifdef _AFXDLL
Enable3dControls();         // Call this when using MFC in
                            // a shared DLL
#else
Enable3dControlsStatic();   // Call this when linking to
                            // MFC statically
#endif
// Change the registry key under which your settings are stored.
// TODO: You should modify this string to be something appropriate
// such as the name of your company or organization.
SetRegistryKey(_T(“Local AppWizard-Generated Applications”));
LoadStdProfileSettings();  // Load standard INI file options
                           // (including MRU)
// Register the application’s document templates.  Document
// templates serve as the connection between documents,
// frame windows and views.
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CMFCServerDoc),
RUNTIME_CLASS(CMainFrame),       // main SDI frame window
RUNTIME_CLASS(CMFCServerView));
AddDocTemplate(pDocTemplate);
// Parse command line for standard shell commands, DDE, file open
CCommandLineInfo cmdInfo;
ParseCommandLine(cmdInfo);
// Dispatch commands specified on the command line
if (!ProcessShellCommand(cmdInfo))
return FALSE;
// The one and only window has been initialized, so show and
// update it.
m_pMainWnd->ShowWindow(SW_SHOW);
m_pMainWnd->UpdateWindow();
return TRUE;
}


////////////////////////////////////////////////////////////////////
// CAboutDlg dialog used for App About
(“About” code removed for clarity...)

//////////////////////////////////////////////////////////////////
// CMFCServerApp message handlers

CMFCServerModule _Module;
BEGIN_OBJECT_MAP(ObjectMap)
OBJECT_ENTRY(CLSID_MFCServer, CMFCServer)
END_OBJECT_MAP()
LONG CMFCServerModule::Unlock()
{
AfxOleUnlockApp();
return 0;
}

LONG CMFCServerModule::Lock()
{
AfxOleLockApp();
return 1;
}
LPCTSTR CMFCServerModule::FindOneOf(LPCTSTR p1, LPCTSTR p2)
{
while (*p1 != NULL)
   {
LPCTSTp = p2;
while (*p != NULL)
      {
if (*p1 == *p)
return CharNext(p1);
p = CharNext(p);
      }
p1++;
   }
return NULL;
}

int CMFCServerApp::ExitInstance()
{
if (m_bATLInited)
   {
_Module.RevokeClassObjects();
_Module.Term();
CoUninitialize();
   }

return CWinApp::ExitInstance();

}

BOOL CMFCServerApp::InitATL()
{
m_bATLInited = TRUE;
#if _WIN32_WINNT >= 0×0400
HRESULT hRes = CoInitializeEx(NULL, COINIT_MULTITHREADED);
#else
HRESULT hRes = CoInitialize(NULL);
#endif
if (FAILED(hRes))

   {
m_bATLInited = FALSE;
return FALSE;
   }

_Module.Init(ObjectMap, AfxGetInstanceHandle());
_Module.dwThreadID = GetCurrentThreadId();

LPTSTR lpCmdLine = GetCommandLine(); //this line necessary for
                                    // _ATL_MIN_CRT
TCHAR szTokens[] = _T(“-/”);
BOOL bRun = TRUE;
LPCTSTR lpszToken = _Module.FindOneOf(lpCmdLine, szTokens);
while (lpszToken != NULL)
   {
if (lstrcmpi(lpszToken, _T(“UnregServer”))==0)
      {
_Module.UpdateRegistryFromResource(IDR_MFCSERVER, FALSE);
_Module.UnregisterServer(TRUE); //TRUE means typelib is
                               // unreg’d
bRun = FALSE;
break;
      }
if (lstrcmpi(lpszToken, _T(“RegServer”))==0)
     {
_Module.UpdateRegistryFromResource(IDR_MFCSERVER, TRUE);
_Module.RegisterServer(TRUE);
bRun = FALSE;
break;
      }
lpszToken = _Module.FindOneOf(lpszToken, szTokens);
   }

if (!bRun)
   {
m_bATLInited = FALSE;
_Module.Term();
CoUninitialize();
return FALSE;
   }
hRes = _Module.RegisterClassObjects(CLSCTX_LOCAL_SERVER,
REGCLS_MULTIPLEUSE);
if (FAILED(hRes))
   {
m_bATLInited = FALSE;
CoUninitialize();
return FALSE;
   }
return TRUE;

}

From the top, after inserting some required header files, the wizard added these lines to InitInstance():

if (!InitATL())
return FALSE;

If the InitATL() member fails, the application itself will terminate.

InitATL() begins its work by initializing the COM library, the specific mechanism for which depends upon the system you’re compiling on (Windows NT 4 or better, or something else, like Windows 98). Assuming that the libraries activated properly, the ATL module is initialized with its object map and thread ID. The ATL object map is similar in concept to MFC’s message map, in that the object map identifies the COM interfaces the module is responsible for servicing. From there, the command line is parsed to determine whether the server is being called to register or unregister. If this is the case, the Registry is updated appropriately and the application terminates (as it should). If the object is being created for use, the module registers its class objects, which are used to create the individual objects the module services (in this case you have only one). In COM terms, it is taking care of the objects’ Class Factories. If this went well, ATL is initialized and the application continues its MFC initialization.

The remainder of InitInstance() is standard MFC application initialization code, with the exception of these lines (added by the wizard):

if (cmdInfo.m_bRunEmbedded || cmdInfo.m_bRunAutomated)
   {
return TRUE;
   }



When the MFC application is invoked by COM, the command line passed to the application will indicate it is embedded. In this case, InitInstance() will return (with a successful result). At this point, the MFC code required to create a new document and to show the window has been skipped.


Caution:  

At this point, you have no document, no view, and no frame window, so do not make calls to these C++ objects from within your COM methods. To do this, you must either modify the default wizard-based code to allow the window to be displayed or you handle things otherwise in your COM method(s), such as creating a particular MFC object yourself.


InitInstance() is completed for you whenever you run the MFC AppWizard. However, in this case you not only need InitInstance(), but you also require ExitInstance(). You must undo what you have done:

int CMFCServerApp::ExitInstance()
{
if (m_bATLInited)
   {
_Module.RevokeClassObjects();
_Module.Term();
CoUninitialize();
   }
return CWinApp::ExitInstance();
}

When the application exits, ExitInstance() will handle the details of revoking the class objects (the Class Factories mentioned previously) and terminating the ATL module. Then, of course, COM itself is shut down.

The wizard also added several other helper functions that are used when initializing and terminating the application. The lock and unlock helper functions serve to increment and decrement the application’s active object count. When the count reaches zero, the application exits:

LONG CMFCServerModule::Unlock()
{
AfxOleUnlockApp();
return 0;
}

LONG CMFCServerModule::Lock()
{
AfxOleLockApp();
return 1;
}

To determine whether the application requires registration or unregistration, the wizard added a helper function used to scan the command line for the specific COM command-line parameters RegServer and UnregServer:

LPCTSTR CMFCServerModule::FindOneOf(LPCTSTR p1, LPCTSTR p2)
{
while (*p1 != NULL)
   {
LPCTSTR p = p2;
while (*p != NULL)
      {
if (*p1 == *p)
return CharNext(p1);
p = CharNext(p);
      }
p1++;
   }
return NULL;
}

The final changes the wizard made were to add the actual ATL module and object map. The ATL module is the piece that actually serves as the COM server. The module uses the array of objects, listed in the object map, to maintain the set of Class Factories for each object in the list. This enables the module to handle such important details as individual object registration and unregistration, reference counting, establishing the communications between the client and the individual object, and, of course, object creation through its Class Factory:

CMFCServerModule _Module;
BEGIN_OBJECT_MAP(ObjectMap)
OBJECT_ENTRY(CLSID_MFCServer, CMFCServer)
END_OBJECT_MAP()

To compile the ATL code you have just examined, the wizard had to add information to the MFC application’s main precompiled header files, stdafx.h and stdafx.cpp. In the case of stdafx.h, it adds a definition to compile the COM code as apartment-threaded (remember your selection when you ran the wizard?), adds the requisite ATL base header files, and declares your ATL module, CMFCServerModule:

#define _ATL_APARTMENT_THREADED
#include <atlbase.h>
//You may derive a class from CComModule and use it if you want
//to override something, but do not change the name of _Module
class CMFCServerModule : public CComModule
{
public:
LONG Unlock();
LONG Lock();
LPCTSTR FindOneOf(LPCTSTR p1, LPCTSTR p2);
DWORD dwThreadID;
};
extern CMFCServerModule _Module;
#include <atlcom.h>

The wizard then added code to stdafx.cpp to add static Registry support (don’t use ATL.DLL) and the usual atlimpl.cpp file you’ve seen before:

#ifdef _ATL_STATIC_REGISTRY
#include <statreg.h>
#endif
#include <atlimpl.cpp>

Finally, the wizard created several new files for you, modified your resource file, and changed your project settings. The files added include your COM interface definition file, MFCServer.idl, the COM interface files themselves, MFCCOMServer.h and MFCCOMServer.cpp, and the Registry script file MFCServer.rgs. The IDL file defines your interface(s) and will be compiled by a special compiler to produce several output files, which will be discussed shortly. The interface files are where you’ll add the true COM functionality for each of your interface member functions. The Registry script file is compiled into a binary form and used by ATL to register and unregister your application as a whole as well as the individual interfaces themselves. (This alone is a handy benefit when using ATL.)

The resource file was modified in two ways: the binary form of the Registry script was added, and a COM type library was inserted. The type library is a compiled, tokenized form of your IDL source file that allows COM clients to scan your object to determine, at runtime, what your COM interfaces do. This is used, in most cases, for scripting purposes.

Your project settings were modified to include a custom build step for the MFCServer.idl file (using Visual C++ 6’s new MIDL settings tab). IDL files are compiled by a special compiler, which in this case is the Microsoft IDL compiler, or MIDL for short. MIDL produces, as output, several files you’ll be required to compile and execute for the COM application. First, the basic C++ files declaring the COM object are generated—MFCServer_i.cpp and MFCServer_i.h. These files define and declare the COM class identifier (CLSID) and interface identifier (IID) values, which you will require when compiling code that uses your COM object(s). MIDL then produces the type library, MFCServer.tlb, which is compiled into your application resources. And finally, MIDL produces source files you will need to generate a proxy/stub DLL.

Interestingly, the ATL Object Wizard did all of this for you, yet it didn’t go far enough. If you were to compile this COM application, and then develop an application that used this COM server, the CoCreateInstance() would fail with the HRESULT E_NOINTERFACE. Why?

Adding the Additional ATL COM Support Files

To understand this, go back to the basic COM principle that COM, among other things, serves as a data conduit between address spaces. You have the two applications you require, the COM server and your client application, but how will they actually communicate? The mechanism the COM marshaling code uses is the proxy/stub DLL, which you can imagine as a “modem” between the two address spaces. The ATL Object Wizard inserted the project settings to build the source code for creating the proxy/stub DLL, but it didn’t add any support for actually building the DLL. To me, this is a huge oversight and a bug.

However, you can recover relatively easily. You need to create two files from scratch and then modify your project settings to actually compile the proxy/stub source code into the DLL itself. The two files you need are a makefile and the DLL definition (DEF) file.


Tip:  

In practice, you would typically find another COM-based project and copy its files for editing rather than actually type the source code from scratch.




For this example, I created the proxy/stub makefile, MFCServerps.mk and inserted the code in Listing 16.5.

Listing 16.5 MFCServerps.mk Proxy/Stub Makefile


MFCServerps.dll: dlldata.obj MFCServer_p.obj MFCServer_i.obj
link /dll /out:MFCServerps.dll /def:MFCServerps.def /entry:DllMain
Ä dlldata.obj MFCServer_p.obj MFCServer_i.obj kernel32.lib
Ä rpcndr.lib rpcns4.lib rpcrt4.lib oleaut32.lib uuid.lib
.c.obj:
cl /c /Ox /DWIN32 /D_WIN32_WINNT=0×0400 /DREGISTER_PROXY_DLL $<
clean:
@del MFCServerps.dll
@del MFCServerps.lib
@del MFCServerps.exp
@del dlldata.obj
@del MFCServer_p.obj
@del MFCServer_i.obj

Then, because you’re compiling a DLL, you need to create the DEF file to indicate what DLL functions are to be visible to external clients (see Listing 16.6).

Listing 16.6 MFCServerps.def Proxy/Stub DLL Export Definition File


LIBRARY      “MFCServerps”
DESCRIPTION  ‘Proxy/Stub DLL’
EXPORTS
   DllGetClassObject       @1 PRIVATE
   DllCanUnloadNow         @2 PRIVATE
   GetProxyDllInfo         @3 PRIVATE
   DllRegisterServer       @4 PRIVATE
   DllUnregisterServer     @5 PRIVATE

The proxy/stub DLL exports the standard COM methods.

Modifying Your Project Settings

Now that you have all of the proxy/stub files you require, you need to add a project setting to actually compile and register the proxy/stub DLL. To do this, you right-click on the project name in the Workspace window, or select Project, Settings from the Developer Studio menu, and add a post-build step, as shown in Figure 16.8.


Figure 16.8  Proxy/stub post-build step.

Essentially what you’re doing is running the nmake command-line compiler against your proxy/stub makefile. Because the typical proxy/stub makefile uses the extension mk rather than the standard mak, you have to use the nmake -f option. You also handle the DLL registration by calling regsvr32.exe. Nothing atypical here.

With this final step, assuming your project source files have no syntax errors, you can compile the source code and build your application executable and proxy/stub DLL.


Note:  

Don’t forget that the application must be registered by running it with the -RegServer command-line option. If you forget to do this, you’ll receive the error HRESULT REGDB_E_CLASSNOTREG (0×80040154).


Building a Client Application

Having compiled and registered your COM server, it’s time to actually use it to do something useful. In this case, create your MFC application as you normally would, by using the MFC AppWizard. After you have a basic framework established, you then add any of the ATL support files (such as for smart pointers). After this is done, you have to decide when and where to create the COM object and insert the CoCreateInstance() call, as well as individual interface member calls where appropriate.

For demonstration purposes, I created a test application MFCServerTest, which is a simple dialog-based test driver (see Figure 16.9).


Figure 16.9  The MFCServerTest application.

The MFCServer object is created in OnInitDialog(). I decided to create it here because this would keep the COM server around throughout the lifetime of the test application. In this manner, when you increment the counter (using the BumpCount member you created previously), you see the count increase. Had you created the object when you clicked the Test button, you would always see a count value of one. The server would be created, increment its count from zero to one, return that value, and then be released. The count you would see displayed would never increase past one. Listing 16.7 is the code to create the MFCServer object.

Listing 16.7 MFCServerTest’s OnInitDialog() Member


////////////////////////////////////////////////////////////////////
// CMFCServerTestDlg message handlers
BOOL CMFCServerTestDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Add “About...” menu item to system menu.

// IDM_ABOUTBOX must be in the system command range.
ASSERT((IDM_ABOUTBOX & 0×FFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0×F000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
   {
CString strAboutMenu;
strAboutMenu.LoadString(IDS_ABOUTBOX);
if (!strAboutMenu.IsEmpty())
      {
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
      }
   }

// Set the icon for this dialog.  The framework does this
// automatically when the application’s main window is not a dialog
   SetIcon(m_hIcon, TRUE);    // Set big icon
   SetIcon(m_hIcon, FALSE);   // Set small icon

// Create your COM object
HRESULT hr = CoCreateInstance(CLSID_MFCServer,
NULL, // no aggregation
CLSCTX_LOCAL_SERVER,
IID_IMFCServer,
reinterpret_cast<void**>(&m_pIMFCServer));
if ( FAILED(hr) ) {
// Problem creating the COM object..
AfxMessageBox(“Error creating MFCServer object”,
               MB_OK|MB_ICONERROR);
EndDialog(IDCANCEL);
return TRUE;
} // if
return TRUE;  // return TRUE  unless you set the focus to a control
}

If you failed to create the object, you’ll display an error message and terminate the dialog box. Otherwise, dialog initialization continues, and you can actually call the COM server by clicking the Test button. The values CLSID_MFCServer and IID_IMFCServer came from the files MIDL created when you compiled your COM server. You must include those same files in this project. I elected to include the definition file in stdafx.h:

// Your COM object interface definitions
#include “..\MFCServer\MFCServer_i.h”

Similarly, I included the interface declarations in stdafx.cpp:

// COM Object GUID definitions
#include “..\MFCServer\MFCServer_i.c”

Feel free to include them anywhere it makes the most sense. My decision to include them in the precompiled header files was simply to allow them to be visible to all of the source files in the project, but your project’s needs may not require (or desire) this.

When you’ve successfully compiled and debugged your test application, you’re done. Well, you’re done with the initial testing, anyway. Now you can get to the real work at hand, which is to integrate the COM server functionality into your main application.

Summary

In this chapter you examined techniques for adding ATL COM support to an MFC application. As you’ll see, this will prove useful to you when you examine other Microsoft technologies such as adding scripting capabilities to your applications (Chapter 17, “Scripting Your MFC Application”); using COM-based technologies, such as Dynamic HTML (Chapter 23); DirectX (Chapter 30, “MFC and DirectX”); and, in some cases, shell programming.